import { Injectable, Logger } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Account } from "./dao";
import { ILike, QueryFailedError, Repository } from "typeorm";
import { InjectMapper } from "@automapper/nestjs";
import { Mapper } from "@automapper/core";
import { CryptoService } from "@/crypto";
import {
    FilterAccountDTO,
    ReadAccountListDTO,
    ReadAccountDTO,
    CreateAccountDTO,
    UpdateAccountDTO,
    UpdateSelfDTO,
    UpdatePasswordDTO
} from "./dto";
import { parseQueryFailedError } from "@/helpers/sql";

@Injectable()
export class AccountsService {
    private readonly logger: Logger = new Logger(AccountsService.name);

    constructor(
        @InjectRepository(Account) private readonly accountRepository: Repository<Account>,
        @InjectMapper() private readonly mapper: Mapper,
        private readonly cryptoService: CryptoService
    ) {}

    async findAll(dto: FilterAccountDTO): Promise<ReadAccountListDTO> {
        this.logger.log(`Trying to get a list of accounts with payload: ${JSON.stringify(dto)}`);
        const conditions = {};
        for (const key of ["userName", "firstName", "lastName", "email"]) {
            if (typeof dto[key] === "string") {
                conditions[key] = ILike(`%${dto[key]}`);
            }
        }
        const [entities, total] = await this.accountRepository.findAndCount({
            where: conditions,
            skip: dto.offset,
            take: dto.limit,
            order: {
                [dto.sortBy]: dto.sortDirection
            }
        });
        const accounts = this.mapper.mapArray(entities, Account, ReadAccountDTO);
        this.logger.log(`Found ${total} accounts. Will be returned: ${JSON.stringify(accounts)}`);
        return {
            total,
            accounts
        };
    }

    async findById(id: number): Promise<ReadAccountDTO> | never {
        try {
            this.logger.log(`Trying to get an account with ID: ${id}`);
            const entity = await this.accountRepository.findOneByOrFail({ id });
            const account = this.mapper.map(entity, Account, ReadAccountDTO);
            this.logger.log(`Found and will be returned: ${JSON.stringify(account)}`);
            return account;
        } catch (e) {
            this.logger.error(`Error when trying to get an account with ID: ${id}. Message: ${e.message}`);
            throw new Error(`Учетная запись с ID ${id} не найдена!`);
        }
    }

    async findByUserName(userName: string): Promise<ReadAccountDTO> | never {
        try {
            this.logger.log(`Trying to get an account with user name: ${userName}`);
            const entity = await this.accountRepository.findOneByOrFail({ userName });
            const account = this.mapper.map(entity, Account, ReadAccountDTO);
            this.logger.log(`Found and will be returned: ${JSON.stringify(account)}`);
            return account;
        } catch (e) {
            this.logger.error(`Error when trying to get an account with user name: ${userName}. Message: ${e.message}`);
            throw new Error(`Учетная запись с именем пользователя ${userName} не найдена!`);
        }
    }

    async searchStudentByName(name: string): Promise<ReadAccountDTO[]> {
        this.logger.log(`Trying to get a list of accounts with role student by name: ${name}`);
        const entities = await this.accountRepository
            .createQueryBuilder("a")
            .where(
                `
                (
                    a.firstName ilike :name or
                    a.firstName ilike :name or
                    a.lastName ilike :name or
                    a.userName ilike :name or
                    a.email ilike :name
                ) and a.roles @> '"student"'`,
                { name: `${name}%` }
            )
            .limit(100)
            .getMany();
        const accounts = this.mapper.mapArray(entities, Account, ReadAccountDTO);
        this.logger.log(`Found and will be returned: ${JSON.stringify(accounts)}`);
        return accounts;
    }

    async create(dto: CreateAccountDTO): Promise<ReadAccountDTO> | never {
        this.logger.log(`Trying to create an account with payload: ${JSON.stringify(dto)}`);
        const entity = this.mapper.map(dto, CreateAccountDTO, Account);
        entity.passwordHash = await this.cryptoService.hashPassword(dto.password);
        try {
            await this.accountRepository.save(entity);
        } catch (e) {
            this.logger.error(`Error when trying to create an account. Message: ${e.message}`);
            if (e instanceof QueryFailedError) {
                throw new Error(parseQueryFailedError(e));
            } else {
                throw new Error("Что-то пошло не так");
            }
        }
        const account = this.mapper.map(entity, Account, ReadAccountDTO);
        this.logger.log(`Account created and will be returned: ${JSON.stringify(account)}`);
        return account;
    }

    async update(id: number, dto: UpdateSelfDTO): Promise<ReadAccountDTO> | never {
        this.logger.log(`Trying to create an account with payload: ${JSON.stringify(dto)}`);
        const entity = this.mapper.map(dto, UpdateAccountDTO, Account);
        if (dto instanceof UpdateAccountDTO && dto.password) {
            entity.passwordHash = await this.cryptoService.hashPassword(dto.password);
            delete dto["password"];
        }
        try {
            await this.accountRepository.update({ id }, entity);
            const account = this.mapper.map(
                await this.accountRepository.findOneByOrFail({ id }),
                Account,
                ReadAccountDTO
            );
            this.logger.log(`Account updated and will be returned: ${JSON.stringify(account)}`);
            return account;
        } catch (e) {
            this.logger.error(`Error when trying to update an account with ID: ${id}. Message: ${e.message}`);
            throw new Error(`Учетная запись с ID ${id} не найдена!`);
        }
    }

    async updatePassword(id: number, dto: UpdatePasswordDTO): Promise<ReadAccountDTO> | never {
        this.logger.log(`Trying to update account password with payload: ${JSON.stringify(dto)}`);
        const oldAccount = await this.accountRepository.findOneBy({ id });
        if (oldAccount) {
            if (await this.cryptoService.checkPasswordHash(dto.oldPassword, oldAccount.passwordHash)) {
                await this.accountRepository.update(
                    { id },
                    {
                        passwordHash: await this.cryptoService.hashPassword(dto.newPassword),
                        needChangePassword: false
                    }
                );
                const account = this.mapper.map(
                    await this.accountRepository.findOneByOrFail({ id }),
                    Account,
                    ReadAccountDTO
                );
                this.logger.log(`Account updated and will be returned: ${JSON.stringify(account)}`);
                return account;
            } else {
                this.logger.error(`Error when trying to update an account with ID: ${id}. Message: wrong password`);
                throw new Error("Неверный пароль!");
            }
        } else {
            this.logger.error(`Error when trying to update an account with ID: ${id}. Message: account not found`);
            throw new Error(`Учетная запись с ID ${id} не найдена!`);
        }
    }

    async deleteById(id: number): Promise<void> | never {
        try {
            this.logger.log(`Trying to delete an account with ID: ${id}`);
            const account = await this.accountRepository.findOneByOrFail({ id });
            await this.accountRepository.softRemove(account);
            this.logger.log(`An account with ID ${id} deleted`);
        } catch (e) {
            this.logger.error(`Error when trying to delete an account with ID: ${id}. Message: ${e.message}`);
            throw new Error(`Учетная запись с ID ${id} не найдена!`);
        }
    }
}
